//
// Created by tang on 2020/6/18.
//

#include "gfx_utility.hpp"

#include <cmath>
#include <opencv2/freetype.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

static cv::Ptr<cv::freetype::FreeType2> gs_ft2;

template<typename T, typename Field>
static inline void to_sorted(T &t1, T &t2, Field f) {
    if (t1.*f > t2.*f)
        std::swap(t1, t2);
}

GFX_Utility::GFX_Utility(IPrinter *printer) : m_printer_{printer}, m_font_height_{8} {

}

GFX_Utility &GFX_Utility::drawLine(Point2D start, Point2D end) {
    to_sorted(start, end, &Point2D::x);
    if (start.x == end.x) { // 竖线，特殊画法
        to_sorted(start, end, &Point2D::y);
        for (int y = start.y; y <= end.y; ++y)
            m_printer_->drawPoint({start.x, y}, false);
    } else {
        // 计算斜率
        double ratio = 1.0 * (end.y - start.y + 1) / (end.x - start.x + 1);
        int pixels = floor(fabs(ratio));
        for (int x = start.x; x <= end.x; ++x) {
            int y = start.y + static_cast<int>(ratio * (x - start.x));
            if (pixels <= 1)
                m_printer_->drawPoint({x, y}, false);
            else {
                drawLine({x, y}, {x, ratio > 0 ? y + pixels : y - pixels});
            }
        }
    }

    return *this;
}

GFX_Utility &GFX_Utility::drawRect(Rect2D rect) {
    // top
    drawLine({rect.x, rect.y}, {rect.x + rect.w - 1, rect.y});

    // bottom
    drawLine({rect.x, rect.y + rect.h - 1}, {rect.x + rect.w - 1, rect.y + rect.h - 1});

    // left
    drawLine({rect.x, rect.y}, {rect.x, rect.y + rect.h - 1});

    // right
    drawLine({rect.x + rect.w - 1, rect.y}, {rect.x + rect.w - 1, rect.y + rect.h - 1});
    return *this;
}

GFX_Utility &GFX_Utility::drawTrangle(Point2D p1, Point2D p2, Point2D p3) {
    drawLine(p1, p2);
    drawLine(p2, p3);
    drawLine(p1, p3);
    return *this;
}

GFX_Utility &GFX_Utility::drawCircle(Point2D center, int radius) {
    if (radius > 0) {
        // x^2 + y^2 = r^2
        m_printer_->drawPoint(center, false);
        const int r2 = radius * radius;
        {// x-dir
            int x = center.x - radius;
            for (; x <= center.x + radius; ++x) {
                const int delta_x2 = static_cast<int>(pow(abs(center.x - x), 2));
                const int y = ceil(sqrt(r2 - delta_x2));
                m_printer_->drawPoint({x, center.y - y}, false);
                m_printer_->drawPoint({x, center.y + y}, false);
            }
        }
        {// y-dir
            int y = center.y - radius;
            for (; y <= center.y + radius; ++y) {
                const int delta_y2 = static_cast<int>(pow(abs(center.y - y), 2));
                const int x = ceil(sqrt(r2 - delta_y2));
                m_printer_->drawPoint({center.x - x, y}, false);
                m_printer_->drawPoint({center.x + x, y}, false);
            }
        }
    }
    return *this;
}

GFX_Utility &GFX_Utility::fillRect(Rect2D rect) {
    for (int i = 0; i < rect.h; ++i)
        drawLine({rect.x, rect.y + i}, {rect.x + rect.w, rect.y + i});
    return *this;
}

GFX_Utility &GFX_Utility::fillTrangle(Point2D p1, Point2D p2, Point2D p3) {
    return *this;
}

GFX_Utility &GFX_Utility::fillCircle(Point2D center, int radius) {
    if (radius > 0) {
// x^2 + y^2 = r^2
        m_printer_->drawPoint(center, false);
        const int r2 = radius * radius;
        {// x-dir
            int x = center.x - radius;
            for (; x <= center.x + radius; ++x) {
                const int delta_x2 = static_cast<int>(pow(abs(center.x - x), 2));
                const int y = ceil(sqrt(r2 - delta_x2));
                drawLine({x, center.y - y}, {x, center.y + y});
            }
        }
        {// y-dir
            int y = center.y - radius;
            for (; y <= center.y + radius; ++y) {
                const int delta_y2 = static_cast<int>(pow(abs(center.y - y), 2));
                const int x = ceil(sqrt(r2 - delta_y2));
                drawLine({center.x - x, y}, {center.x + x, y});
            }
        }
    }
    return *this;
}

GFX_Utility &GFX_Utility::drawText(const std::string &text, Rect2D renderArea, TextAliment aliment) {
    using namespace cv;

    // measure text width
    int baseline = 0;
    Size s = gs_ft2->getTextSize(text, m_font_height_, -1, &baseline);
    const Scalar fontColor{255, 255, 255, 255};
    Point startPos;

    // choose line types
    int line_type = -1;
    if (m_font_height_ < 16)
        line_type = LINE_4;
    else
        line_type = LINE_8;

    if (s.height == 1)
    {
        baseline = ceil(m_font_height_ * 1.3333);
        s.height = ceil(m_font_height_ * 0.75);
    }

    switch (aliment) {
        case Left:
            startPos = {0, (renderArea.h + s.height) / 2};
            break;
        case Right:
            startPos = {renderArea.w - s.width, (renderArea.h + s.height) / 2};
            break;
        case Center:
            startPos = {(renderArea.w - s.width) / 2, (renderArea.h + s.height) / 2};
            break;
        default:
            break;
    }

    Mat colored{Size{renderArea.w, renderArea.h}, CV_8UC3, {0, 0, 0}};
    gs_ft2->putText(colored, text, startPos, m_font_height_, fontColor, -1, line_type, true);

    Mat gray{colored.size(), CV_8UC1, Scalar{0}};
    cvtColor(colored, gray, COLOR_RGB2GRAY, 1);
    for (int y = 0; y < renderArea.h; y++) {
        for (int x = 0; x < renderArea.w; ++x) {
            if (gray.at<unsigned char>({x, y}) > 0)
                m_printer_->drawPoint({renderArea.x + x, renderArea.y + y});
        }
    }

    return *this;
}

GFX_Utility &GFX_Utility::drawImage(const std::string &path, Rect2D renderArea) {
    using namespace cv;

    // 先进行缩放
    Mat origin = imread(path);
    if (!origin.empty()) {
        Mat scaled;
        resize(origin, scaled, {renderArea.w, renderArea.h});
        Mat gray{Size{renderArea.w, renderArea.h}, CV_8UC1};
        cvtColor(scaled, gray, COLOR_RGB2GRAY);

        for (int y = 0; y < renderArea.h; y++) {
            for (int x = 0; x < renderArea.w; ++x) {
                if (gray.at<unsigned char>({x, y}) > 0x7F)
                    m_printer_->drawPoint({renderArea.x + x, renderArea.y + y});
            }
        }
    }
    return *this;
}

void GFX_Utility::commit() {
    m_printer_->display();
}

GFX_Utility &GFX_Utility::reset() {
    m_printer_->clear();
    return *this;
}

void GFX_Utility::loadFont(const std::string &file, int index) {
    if (gs_ft2.empty())
        gs_ft2 = cv::freetype::createFreeType2();
    gs_ft2->loadFontData(file, index);
}

GFX_Utility &GFX_Utility::fontSize(int height) {
    m_font_height_ = height;
    return *this;
}
